Unix网络编程-第3章 套接字编程简介

3.2 套接字的地址结构

大多数套接字函数都需要一个指向套接字地址结构的指针作为参数。每个协议族都定义自己的套接字地址结构,这些结构的名字均已sockaddr_开头,并以对应每个协议族的唯一后缀结尾。

IPv4套接字地址结构:

struct in_addr{
    in_addr_t          s_addr;
};
struct sockaddr_in{
    uint8_t            sin_len;
    
    //POSIX规范只需要这个结构中的三个字段
    sa_family_t        sin_family;
    in_port_t          sin_port;
    struct in_addr     sin_addr;
    
    char               sin_zero[8];
};
字段数据类型说明
s_addrin_addr_t至少32位的无符号整数类型
sin_portin_port_t至少16位的无符号整数类型
sin_familysa_family_t任何无符号整数类型。在支持长度字段的实现中,通常是一个8位无符号整数,在不支持长度字段中,是一个16位的无符号整数

套接字地址结构仅在给定主机上使用:虽然结构中某些字段用在不同主机之间的通信,但是结构本身并不在主机之间传递。

为了让套接字函数能够处理来自任何协议族的套接字地址结构,套接字函数定义的参数中使用指向通用套接字地址结构的指针,使用时再进行类型强制转换

通用套接字地址结构:

struct sockaddr{
    uint8_t            sa_len;		//该字段只在一些Unix实现中有
	    						  	//SuSv3标准不做要求,Linux实现也不存在该字段
    sa_family_t        sa_family;
    char               sa_date[14];
};

IPv6套接字地址结构:

struct in6_addr{
    uint8_t            s6_addr[16];
};

//如果系统支持套接字地址结构中的长度字段,则SIN6_LEN常值必须定义
#define SIN6_LEN

struct sockaddr_in6{
    uint8_t            sin6_len;
    sa_family_t        sin6_family;
    in_port_t          sin6_port;
    
    uint32_t           sin6_flowinfo;
    struct in6_addr    sin6_addr;
    
    uint32_t           sin6_scope_id;
}

新的通用套接字地址结构

struct sockaddr_storage{
    uint8_t        ss_len;
    sa_family_t    ss_family;
}

套接字地址结构比较:

1579573720073

3.3 值-结果参数

套接字的地址结构总是以引用形式传递给套接字函数的。

套接字的长度作为一个参数传递给套接字函数时,其传递方式取决于该结构的传递方向。

套接字地址结构可以在两个方向上传递:

  • 从进程到内核。函数:bind、connect、sendto。

    这些函数的一个参数是指向套接字地址结构的指针,另一个参数是该结构的整数大小。

  • 从内核到进程。函数:accept、recvfrom、getsockname、getpeername。

    这些函数的一个参数是指向套接字地址结构的指针,另一个参数是指向表示该结构大小的整数变量的指针(这种类型的参数称为“值-结果”参数)。

    值-结果传参:
    当函数被调用时,结构大小是一个值,它告诉内核该结构的大小,这样内核在写该结构时不至于越界。
    当函数返回时,结构大小又是一个结果,它告诉进程内核在该结构中究竟存储了多少信息。
    

当套接字地址结构的长度使用值-结果参数时,如果套接字地址结构是固定长度则从内核返回的值总是那个长度,如果是可变长度,则返回值可能小于该结构的最大长度。

3.4 字节排序函数

主机字节序:

  • 小端字节序:将低序字节存储在起始地址
  • 大端字节序:将高序字节存储在起始地址

1579582483045

最高有效位:MSB:most significant bit

最低有效位:LSB:least significant bit

术语“小端”和“大端”表示:多个字节值的哪一端(小端或大端)存储在该值的起始地址(低地址)。

网络字节序:大端字节序

网络协议必须指定一个网络字节序。由于历史原因和POSIX规范的规定,套接字地址结构中的某些字段必须按照网络字节序进行维护。

主机字节序和网络字节序之间相互转换使用以下4个函数:

  • s视为一个16位的值,例如TCP或UDP的端口号
  • l视为一个32位的值,例如IPv4地址
  • 主机字节序和网络字节序相同的系统中这四个函数定义为空宏
//主机:host(h)
//网络:network(n)
//短整型:short(s)
//长整型:long(l)
#include <netinet/in.h>

//返回网络字节序的值
uint16_t htons(uint16_t host16bitvalue);
uint32_t htonl(uint32_t host32bitvalue);

//返回主机字节序的值
uint16_t ntohs(uint16_t net16bitvalue);
uint32_t ntohl(uint32_t net32bitvalue);

因特网另一个重要的约定是位序,IPv4首部前32位的位序如下:

1579584598760

3.5 字节操作函数

操作多字节段的函数有两组,它们既不对数据作解释,也不假设数据是以空字节符结束的C字符串。

  • 第一组函数源于4.2BSD,名字以b(表示字节)开头

    #include <strings.h>
    
    void bzero(void *dest, size_t nbytes);
    void bcopy(const void *src, void *dest, size_t nbytes);
    //若相等则返回0,否则为非0
    int bcmp(const void *ptr1, const void *ptr2, size_t nbytes);
    
  • 第二组函数源于ANSI C标准,名字以men(表示内存)开头

    #include <string.h>
    
    //每个函数的最后一个参数都是长度参数
    void *memset(void *dest, int c, size_t len);
    //memcpy函数的参数顺序与C的赋值语句顺序相同:dest = src
    void *memcpy(void *dest, const void *src, size_t nbytes);
    //若相等则返回0,否则
    //    看第一个不等字节:ptr1 > ptr2,则返回值大于0,否则返回值小于0
    int memcmp(const void *ptr1, const void *ptr2, size_t nbytes);
    

当源字节串与目标字节串重叠时,bcopy能够正确处理,memcpy的操作结果却不可知,这种情况必须改用ANSI C的memmove函数。

比较操作是假设两个不等字节均为无符号字符(unsigned char)的情况下完成的。

3.6 inet_aton、inet_addr和inet_ntoa函数

功能介绍:在点分十进制数串和它长度为32位的网络字节序二进制值间转换IPv4地址

#include <arpa/inet.h>

//若字符串有效,则返回1,否则返回0
//如果addrptr指针为空,那么该函数仍然对输入的字符串执行有效性检查,但是不存储任何结果。
int inet_aton(const char *strptr, struct in_addr *addrptr);

//若字符串有效,则返回32位二进制网络字节序的IPv4地址,否则返回INADDR_NONE
//NADDR_NONE常值通常是一个32位均为1的值,这意味着点分十进制数串255.255.255.255不能由该函数处理,因为其二进制值被用来指示函数失败。
in_addr_t inet_addr(const char *strptr);

//返回一个点分十进制数串的指针
char *inet_ntoa(struct in_addr inaddr);

3.7 inet_pton和inet_ntop函数

这两个函数对于IPv4地址和IPv6地址都适用。函数名中p和n分别代表表达(presentation)数值(numeric)

#include <arpa/inet.h>

//函数执行成功返回1,表达的格式无效返回0,出错返回-1
int inet_pton(int family, const char *strptr, void *addptr);

//函数执行成功返回指向结果的指针,出错返回NULL
const char *inet_ntop(int family, const void *addptr, char *strptr, size_t len);

//family参数可以是AF_INET,也可以是AF_INET6,如果以不被支持的地址族作为family参数,两个函数就都返回一个错误,并将errno置为EAFNOSUPPORT

总结5个函数

1579590914742

3.8 sock_ntop和相关函数

本书编写的协议无关性函数。函数名以sock_开头。

#include "unp.h"

//成功返回非空指针,出错返回NULL
char *sock_ntop(const struct sockaddr * sockaddr, socklen_t addrlen);

//成功返回0,出错返回-1
int sock_bind_wild(int sockfd, int family);

//若地址为同一协议族且相同,则返回0,反则返回非0
int sock_cmp_addr(const struct sockaddr *sockaddr1,
                  const struct sockaddr *sockaddr2, socklen_t addrlen);

//若地址为同一协议族且端口相同,则返回0,反则返回非0
int sock_cmp_addr(const struct sockaddr *sockaddr1,
                  const struct sockaddr *sockaddr2, socklen_t addrlen);

//返回:若为IPv4或IPv6地址则为非负端口号,否则为-1
int sock_get_port(const struct sockaddr *sockaddr, socklen_t addrlen);

//成功返回非空指针,出错返回NULL
char *sock_ntop_host(const struct sockaddr *sockaddr, socklen_t addrlen);

void sock_set_addr(const struct sockaddr *sockaddr, 
                   socklen_t addrlen, void *ptr);
void sock_set_port(const struct sockaddr *sockaddr,
                   socklen_t addrlen, int port);
void sock_set_wild(sturct sockaddr *sockaddr, socklen_t addrlen);

3.9 readn、writen和readline函数

字节流套接字上的read和write函数所表现的行为不同于通常文件的I/O。字节流套接字上调用read或write输入或输出的字节数可能比请求的数量少,然而这不是出错状态。原因在于:内核中用于套接字的缓冲区可能已经达到极限。此时需要的是调用者再次调用read或write函数,输入或输出剩余的字节。

这个现象在read一个字节流套接字时很常见,但是在write一个字节流时只能在该套接字为非阻塞的前提下才出现。

为了预防万一,不让返回的字节计数值不足,编写了三个函数。

#include "unp.h"

ssize_t readn(int filedes, void *buff, size_t nbytes);
ssize_t written(int filedes, const void *buff, size_t nbytes);
ssize_t readline(int filedes, void *buff, size_t maxlen);